
from S3D.Algorithms import binarySearchRange
from S3D.Config import Config
from S3D.Tools.Sound.EventSearcher.Updater import *
from S3D.Tools.Sound.EventSearcher.SettingsDialog import EVENT_PROP_DESC

MAX_SEARCH_COUNT = 100

class WorkerCache(Config):
    VERSION = 5
    PATH    = "EventSearcher.cache"

    def __init__(self):
        self.projects = {}      # workspace <-> {project <-> ProjectInfo}
        self.levels   = {}      # workspace <-> {levels <-> {sounds}
        self.levelsCache = {}   # workspace <-> mod-cache

class ProjectInfo:
    def __init__(self, name, path, lastEditTime, fdpInfo):
        self.projectName    = name
        self.projectPath    = path
        self.lastTime       = lastEditTime

        events, soundDefinitions, soundBanks, waves = fdpInfo

        self.events         = events # EventInfo
        self.definitions    = soundDefinitions
        self.soundBanks     = soundBanks
        self.waves          = waves

        self.resetSearchTables()

    def resetSearchTables(self):
    # search data
        self.nameSearch     = [] # list of pairs (name, event-index)
        self.subNameSearch  = [] # list of pairs (subName, {event-index-set})
        self.paramSearch    = [[] for _ in Sound.EVENT_PROPERTIES]

    def getBankByWave(self, wavIndex):
        for name, (types, waves) in self.soundBanks.items():
            if wavIndex in waves:
                return name
        raise ValueError

    def getInfo(self):
        return self.projectName, self.projectPath, self.lastTime

    def buildTables(self):
        print("Building tables for %s..." % self.projectName)
        # build name search table
        events = self.events
        for index, p in enumerate(events):
            self.nameSearch.append((p.name, index))

            for paramIndex, paramValue in enumerate(p.params):
                self.paramSearch[paramIndex].append((paramValue, index))

            name = p.name
            while name:
                self.subNameSearch.append((name, index))
                name = name[1:]

        self.nameSearch.sort()
        self.subNameSearch.sort()

        for paramTable in self.paramSearch:
            paramTable.sort()

class SearchTask:
    isNeedResolve = True

    def __init__(self, worker, type, data, projects):
        self.worker     = worker
        self.projects   = projects
        self.type       = type
        self.data       = data

    def search(self):
        res = []
        for pr in self.projects:
            self.searchProj(pr, res)

        self.processResults(res)
        return res

    def searchProj(self, proj, res):
        pass

    def processResults(self, res):
        res.sort(key=lambda x: x.name)

class NameSearchTask(SearchTask):
    def processResults(self, res):
        key = unicode(self.data)
        firstKey    = [p for p in key.split(' ') if p]
        firstKey    = firstKey[0] if firstKey else ""
        res.sort(key=lambda x: (x.name.find(firstKey), x.name))

    def searchProj(self, proj, res):
        table   = self.type == "name" and proj.nameSearch or proj.subNameSearch
        key     = unicode(self.data)
        events  = proj.events

        subKeys = [p for p in key.split(' ') if p]
        if not subKeys:
            return

        key     = subKeys[0]
        subKeys = subKeys[1:]
        keyLen  = len(key)

        def checkSubKeys(curKey):
            if not subKeys:
                return True

            curKey = curKey[keyLen:]
            for p in subKeys:
                index = curKey.find(p)
                if index == -1:
                    return False

                curKey = curKey[index + len(p):]
            return True

        try:
            first, last = binarySearchRange(table, key, key=lambda x: x[0][:keyLen])
        except ValueError:
            return

        usedEvents = set()

        lastKey = None
        check = False
        for k in range(first, last + 1):
            curKey, index = table[k]
            if index in usedEvents:
                continue

            if lastKey != curKey:
                lastKey = curKey
                check = checkSubKeys(curKey)

            if not check:
                continue

            usedEvents.add(index)
            res.append(events[index])

class ParamSearchTask(SearchTask):
    def processResults(self, res):
        paramIndex  = self.data[0]
        res.sort(key=lambda x: (x.params[paramIndex], x.name))

    def searchProj(self, proj, res):
        paramIndex, rangeFrom, rangeTo = self.data

        events  = proj.events
        table   = proj.paramSearch[paramIndex]

        if rangeFrom > rangeTo:
            rangeFrom, rangeTo = rangeTo, rangeFrom

        try:
            firstIndex, lastIndex = binarySearchRange(table, rangeFrom, rangeTo, key=lambda x: x[0])
        except ValueError:
            return

        for k in range(firstIndex, lastIndex + 1):
            index = table[k][1]
            res.append(events[index])

class CustomSearchTask(SearchTask):
    isNeedResolve = False

    def search(self):
        tasks = self.data

        res = []
        for pr in self.projects:
            res.extend(pr.events)

        self.worker._resolveEventsInfo(res)

        for task in tasks:
            res = task.filter(res)

        res.sort(key=lambda x: x.name)

        return res


SEARCH_TASKS = dict(
    name    = NameSearchTask,
    subName = NameSearchTask,
    param   = ParamSearchTask,
    custom  = CustomSearchTask)

class Worker(mp.Process):
    def __init__(self, workspace):
        super(Worker, self).__init__()

        self.queue = mp.Queue()
        self.recv_conn, self.send_conn = mp.Pipe(False)

        self.workspace = workspace
        Workspace.setWorkspace(workspace)

        self.lastSearch = None

    #
    # Parent process methods
    #
    def sendMessage(self, message, *data):
        if len(data) == 1:
            self.queue.put((message, data[0]))
        else:
            self.queue.put((message, data))

    def hasReply(self):
        return self.recv_conn.poll()

    def getReply(self):
        return self.recv_conn.recv()

    #
    # Child process methods
    #
    def _setStatus(self, message):
        self.send_conn.send(("status", message))

    def _sendReply(self, message, *data):
        if len(data) == 1:
            self.send_conn.send((message, data[0]))
        else:
            self.send_conn.send((message, data))

    def _loadInfo(self):
        print("Loading cache...")
        self.data = WorkerCache.load()

        for projDict in self.data.projects.values():
            for pr in projDict.values():
                pr.buildTables()

        print("Done.")

    def _saveInfo(self):
        print("Saving cache...")

        for projDict in self.data.projects.values():
            for pr in projDict.values():
                pr.resetSearchTables()

        self.data.save()
        print("Done.")

    def _getProjects(self, workspace=None):
        if workspace is None:
            workspace = self.workspace

        try:
            return self.data.projects[self.workspace]
        except KeyError:
            self.data.projects[self.workspace] = res = {}
            return res

    def _search(self, type, selectedProjects, data):
        if type is None:
            self.lastSearch = None
            self._sendReply("search_result", type, data, [])
            self._setStatus("Ready")
            return

        self.lastSearch = type, selectedProjects, data

        res, isNeedResolve = self._getSearchResults(type, selectedProjects, data)

        found   = len(res)
        res     = res[:MAX_SEARCH_COUNT]

        if isNeedResolve:
            self._resolveEventsInfo(res)

        self._sendReply("search_result", type, data, res)
        self._setStatus("%d events found" % found)

    def _save(self, path, paramList, type, selectedProjects, data):
        self._setStatus("Saving %s..." % path)

        res, isNeedResolve = self._getSearchResults(type, selectedProjects, data)
        if isNeedResolve:
            self._resolveEventsInfo(res)

        _, ext = os.path.splitext(path)
        ext = ext.lower()

        try:
            f = open(path, "w")

            if ext == '.csv':
                # write header
                header = ["Name"]
                header.extend(EVENT_PROP_DESC[index] for index in paramList)
                f.write("%s\n" % ', '.join(header))

                # write elements
                for e in res:
                    line = [e.name]
                    line.extend(e.getProp(index, PROP_CONTEXT_CSV) for index in paramList)
                    f.write("%s\n" % ', '.join(line))
            elif ext == '.txt':
                for e in res:
                    f.write("%s\n" % e.name)
                    for index in paramList:
                        desc = EVENT_PROP_DESC[index]
                        if len(desc) < 32:
                            desc += (16 - len(desc)) * ' '
                        f.write("\t%s =\t%s\n" % (desc, e.getProp(index, PROP_CONTEXT_TXT)))
                    f.write("\n")
            else:
                for e in res:
                    f.write("%s\n" % e.name)

            self._setStatus("Done")
        except IOError as e:
            self._sendReply("save_failed", path, e.message or traceback.format_exc())
            self._setStatus("Failed to save file %s" % path)
        except Exception as e:
            self._setStatus("Error occured: %s" % (e.message or traceback.format_exc()))
            raise
        finally:
            if f: f.close()

    def _getSearchResults(self, type, selectedProjects, data):
        if type not in SEARCH_TASKS:
            print("Worker: Unknown search task: %s" % type)
            return []

        projects = []
        for proj in self._getProjects().values():
            if proj.projectName in selectedProjects:
                projects.append(proj)

        task    = SEARCH_TASKS[type]
        task    = task(self, type, data, projects)

        return task.search(), task.isNeedResolve

    def _resolveEventsInfo(self, events):
        projects = self._getProjects()
        levels   = self.data.levels.get(self.workspace) or {}

        for index, ev in enumerate(events):
            events[index] = ev = ev.copy()

            proj = projects[ev.project]
            ev.banks = banks = set()

            for definition in ev.definitions:
                waves = proj.definitions[definition]
                for wav in waves:
                    banks.add(proj.getBankByWave(wav))

            if len(banks) != 1:
                ev.bankTypes = [SOUNDBANK_TYPE_INVALID for _ in Sound.PS_PLATFORMS]
            else:
                bankName, = banks
                ev.bankTypes, _ = proj.soundBanks[bankName]

            if not levels:
                ev.levels.append("...")
                continue

            curLevels = set()
            for level, levelEvents in levels.items():
                if ev.name in levelEvents:
                    curLevels.add(level)
            ev.levels = list(sorted(curLevels))

    def updateProject(self, workspace, projectName, projectPath, lastTime, fdpInfo):
        projects = self._getProjects(workspace)
        projects[projectName] = projInfo = ProjectInfo(projectName, projectPath, lastTime, fdpInfo)

        projInfo.buildTables()

        if self.lastSearch is not None:
            self._search(*self.lastSearch)

        print("Done.")

    def run(self):
        self._setStatus("Loading cache...")
        self._loadInfo()
        self._setStatus("Ready")

        def getUpdaterProjects():
            projects = {}
            for pr in self._getProjects().values():
                name, path, time = pr.getInfo()
                projects[name] = path, time
            return projects

        self.updater = Updater(self.queue,
           self.workspace,
           getUpdaterProjects(),
           self.data.levelsCache.get(self.workspace))

        self.updater.start()

        print("Ready.")

        while True:
            message, data = self.queue.get()
            if message == "stop":
                break

            try:
                if message == "workspace":
                    self.workspace = data
                    Workspace.setWorkspace(data)
                    self.updater.sendMessage("workspace",
                        data,
                        getUpdaterProjects(),
                        self.data.levelsCache.get(self.workspace))
                elif message == "search":
                    self._search(*data)
                elif message == "save":
                    self._save(*data)
                elif message == "status":
                    self._setStatus(data)
                elif message == "update":
                    self.updateProject(*data)
                elif message == "levels":
                    workspace, levelsCache, stats       = data
                    self.data.levels[workspace]         = stats
                    self.data.levelsCache[workspace]    = levelsCache
                    if self.lastSearch is not None:
                        self._search(*self.lastSearch)
                else:
                    print("Worker: Unknown message: %s" % message)
            except:
                traceback.print_exc()

        self._saveInfo()
